
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: MethodGeneratorSwitch.java 377 2010-03-11 20:50:18Z archie.cobbs $
 */

package org.dellroad.sidekar.classgen;

import static org.dellroad.sidekar.asm.Opcodes.ALOAD;
import static org.dellroad.sidekar.asm.Opcodes.CHECKCAST;
import static org.dellroad.sidekar.asm.Opcodes.ILOAD;
import static org.dellroad.sidekar.asm.Opcodes.INVOKESTATIC;
import static org.dellroad.sidekar.asm.Opcodes.INVOKEVIRTUAL;
import static org.dellroad.sidekar.asm.Opcodes.IRETURN;
import static org.dellroad.sidekar.asm.Opcodes.RETURN;

import java.lang.reflect.Method;

import org.dellroad.sidekar.Item;
import org.dellroad.sidekar.ListField;
import org.dellroad.sidekar.MapField;
import org.dellroad.sidekar.SetField;
import org.dellroad.sidekar.asm.ClassWriter;
import org.dellroad.sidekar.asm.MethodVisitor;
import org.dellroad.sidekar.asm.Type;
import org.dellroad.sidekar.schema.model.ArrayFieldModel;
import org.dellroad.sidekar.schema.model.AtomicFieldModel;
import org.dellroad.sidekar.schema.model.BigIntegerFieldModel;
import org.dellroad.sidekar.schema.model.BooleanFieldModel;
import org.dellroad.sidekar.schema.model.ByteFieldModel;
import org.dellroad.sidekar.schema.model.CharacterFieldModel;
import org.dellroad.sidekar.schema.model.DateFieldModel;
import org.dellroad.sidekar.schema.model.DoubleFieldModel;
import org.dellroad.sidekar.schema.model.FieldModel;
import org.dellroad.sidekar.schema.model.FieldModelSwitch;
import org.dellroad.sidekar.schema.model.FloatFieldModel;
import org.dellroad.sidekar.schema.model.IntegerFieldModel;
import org.dellroad.sidekar.schema.model.ListFieldModel;
import org.dellroad.sidekar.schema.model.LongFieldModel;
import org.dellroad.sidekar.schema.model.MapFieldModel;
import org.dellroad.sidekar.schema.model.ReferenceFieldModel;
import org.dellroad.sidekar.schema.model.SetFieldModel;
import org.dellroad.sidekar.schema.model.ShortFieldModel;
import org.dellroad.sidekar.schema.model.SimpleFieldModel;
import org.dellroad.sidekar.schema.model.StringFieldModel;
import org.dellroad.stuff.java.Primitive;

/**
 * Generates methods in generated classes.
 */
class MethodGeneratorSwitch extends MethodGeneratorSupport implements FieldModelSwitch<Void> {

    private final ClassWriter cw;

    public MethodGeneratorSwitch(String className, ClassWriter cw) {
        super(className);
        this.cw = cw;
    }

    public Void caseBooleanFieldModel(BooleanFieldModel fieldModel) {
        return casePrimitiveFieldModel(fieldModel, Primitive.BOOLEAN);
    }

    public Void caseByteFieldModel(ByteFieldModel fieldModel) {
        return casePrimitiveFieldModel(fieldModel, Primitive.BYTE);
    }

    public Void caseCharacterFieldModel(CharacterFieldModel fieldModel) {
        return casePrimitiveFieldModel(fieldModel, Primitive.CHARACTER);
    }

    public Void caseShortFieldModel(ShortFieldModel fieldModel) {
        return casePrimitiveFieldModel(fieldModel, Primitive.SHORT);
    }

    public Void caseIntegerFieldModel(IntegerFieldModel fieldModel) {
        return casePrimitiveFieldModel(fieldModel, Primitive.INTEGER);
    }

    public Void caseFloatFieldModel(FloatFieldModel fieldModel) {
        return casePrimitiveFieldModel(fieldModel, Primitive.FLOAT);
    }

    public Void caseDoubleFieldModel(DoubleFieldModel fieldModel) {
        return casePrimitiveFieldModel(fieldModel, Primitive.DOUBLE);
    }

    public Void caseLongFieldModel(LongFieldModel fieldModel) {
        return casePrimitiveFieldModel(fieldModel, Primitive.LONG);
    }

    public Void caseStringFieldModel(StringFieldModel fieldModel) {
        return caseSimpleFieldModel(fieldModel);
    }

    public Void caseDateFieldModel(DateFieldModel fieldModel) {
        return caseSimpleFieldModel(fieldModel);
    }

    public Void caseBigIntegerFieldModel(BigIntegerFieldModel fieldModel) {
        return caseSimpleFieldModel(fieldModel);
    }

    public Void caseArrayFieldModel(ArrayFieldModel fieldModel) {
        return caseSimpleFieldModel(fieldModel);
    }

    public Void caseSetFieldModel(SetFieldModel fieldModel) {

        // Get field access method
        Method getMethod;
        try {
            getMethod = SetField.class.getMethod("get", Item.class);
        } catch (Exception e) {
            throw new RuntimeException("internal error", e);
        }

        // Forward get() method
        forwardFieldMethod(fieldModel, fieldModel.getAccessMethod(), getMethod);
        return null;
    }

    public Void caseMapFieldModel(MapFieldModel fieldModel) {

        // Get field access method
        Method getMethod;
        try {
            getMethod = MapField.class.getMethod("get", Item.class);
        } catch (Exception e) {
            throw new RuntimeException("internal error", e);
        }

        // Forward get() method
        forwardFieldMethod(fieldModel, fieldModel.getAccessMethod(), getMethod);
        return null;
    }

    public Void caseListFieldModel(ListFieldModel fieldModel) {

        // Get field access method
        Method getMethod;
        try {
            getMethod = ListField.class.getMethod("get", Item.class);
        } catch (Exception e) {
            throw new RuntimeException("internal error", e);
        }

        // Forward get() method
        forwardFieldMethod(fieldModel, fieldModel.getAccessMethod(), getMethod);
        return null;
    }

    public Void caseReferenceFieldModel(ReferenceFieldModel fieldModel) {
        return caseSimpleFieldModel(fieldModel);
    }

    // Handle primitive fields
    private Void casePrimitiveFieldModel(AtomicFieldModel fieldModel, Primitive prim) {
        return caseSimpleFieldModel(fieldModel, Type.getType(prim.getWrapperType()),
          Type.getType(prim.getType()), prim.getName() + "Value");
    }

    // Handle simple fields in the non-primitive case
    private Void caseSimpleFieldModel(SimpleFieldModel fieldModel) {
        return caseSimpleFieldModel(fieldModel, null, null, null);
    }

    // Handle simple fields with optional primitive wrapper handling
    private Void caseSimpleFieldModel(SimpleFieldModel fieldModel, Type wrapperType, Type primitiveType, String unwrapMethod) {

        // Handle getter method
        Method method = fieldModel.getReadMethod();
        if (method != null) {
            Type returnType = Type.getType(method.getReturnType());
            MethodVisitor mv = this.cw.visitMethod(copyAccess(method), method.getName(),
              Type.getMethodDescriptor(method), null, getExceptionNames(method));
            getField(mv, fieldModel);
            mv.visitVarInsn(ALOAD, 0);
            emitInvoke(mv, this.getMethod);
            if (primitiveType != null) {
                mv.visitTypeInsn(CHECKCAST, wrapperType.getInternalName());
                mv.visitMethodInsn(INVOKEVIRTUAL, wrapperType.getInternalName(), unwrapMethod,
                  Type.getMethodDescriptor(primitiveType, new Type[0]));
            } else
                mv.visitTypeInsn(CHECKCAST, returnType.getInternalName());
            mv.visitInsn(returnType.getOpcode(IRETURN));
            mv.visitMaxs(0, 0);
            mv.visitEnd();
        }

        // Handle setter method
        method = fieldModel.getWriteMethod();
        if (method != null) {
            MethodVisitor mv = this.cw.visitMethod(copyAccess(method), method.getName(),
              Type.getMethodDescriptor(method), null, getExceptionNames(method));
            getField(mv, fieldModel);
            mv.visitVarInsn(ALOAD, 0);
            if (primitiveType != null) {
                mv.visitVarInsn(primitiveType.getOpcode(ILOAD), 1);
                mv.visitMethodInsn(INVOKESTATIC, wrapperType.getInternalName(), "valueOf",
                  Type.getMethodDescriptor(wrapperType, new Type[] { primitiveType }));
            } else
                mv.visitVarInsn(ALOAD, 1);
            emitInvoke(mv, this.setMethod);
            mv.visitInsn(RETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();
        }

        // Done
        return null;
    }

    private void forwardFieldMethod(FieldModel fieldModel, Method from, Method to) {
        forwardFieldMethod(fieldModel, from, to, true, null);
    }

    /**
     * Emit a method that forwards a method call to an associated {@link Field}:
     *
     *  <blockquote>
     *  {@code this.from(a, b, c, ...) -&gt; this.xxx_field.to(this, a, b, c, ...)}
     *  </blockquote>
     *
     * <p>
     * If {@code from} is null, nothing is emitted.
     */
    private void forwardFieldMethod(final FieldModel fieldModel, Method from, Method to,
      final boolean passItem, final Type castTo) {
        CodeEmitter pre = new CodeEmitter() {
            public void write(MethodVisitor mv) {
                getField(mv, fieldModel);
                if (passItem)
                    mv.visitVarInsn(ALOAD, 0);
            }
        };
        CodeEmitter post = castTo == null ? null : new CodeEmitter() {
            public void write(MethodVisitor mv) {
                mv.visitTypeInsn(CHECKCAST, castTo.getInternalName());
            }
        };
        forwardMethod(this.cw, from, to, pre, post);
    }
}

